Skip to content

fix: expired OAuth sessions must 401 instead of silently degrading - #21

Merged
ConalMullan merged 1 commit into
developfrom
fix/expired-oauth-session-401
Aug 12, 2026
Merged

fix: expired OAuth sessions must 401 instead of silently degrading#21
ConalMullan merged 1 commit into
developfrom
fix/expired-oauth-session-401

Conversation

@ConalMullan

Copy link
Copy Markdown
Collaborator

Problem

When an OAuth session expired, authMiddleware fell through to the legacy direct-developer-key branch and passed the dead session ID to the Digital Samba API as if it were a key.

The client therefore received the API's "Unauthenticated." as a tool error rather than a 401 — so it never learned to re-authorise, and could not recover even on reconnect. The only remedy available to a user was to remove and re-add the connector.

The existing 401 branch was unreachable on that path: sessionId was only set when a session had already been found.

Sessions have a 24h TTL (TTL.SESSION), so every hosted customer hits this daily.

Found while smoke-testing after #19/#20: reconnecting the dev connector produced Unauthenticated with oauthSessions: 0 and zero mcp:session: keys in Redis — the transport reconnected, but OAuth was never re-run because nothing told the client to.

Change

Recognise tokens shaped like the session IDs we issue and treat them as sessions even when expired:

  • Session IDs: randomBytes(32).toString("hex") → 64 lowercase hex, no dashes
  • Developer keys: UUIDs

The two shapes never collide, so legacy direct-key usage is unaffected.

Expired sessions now return 401 with WWW-Authenticate: Bearer realm="mcp", error="invalid_token", ... resource_metadata="/.well-known/oauth-protected-resource" — the standard cue for the client to re-run OAuth itself.

Verification

Against a locally built server in NODE_ENV=production:

Request Result
Expired-session-shaped token 401 + WWW-Authenticate
UUID developer key 200, session issued, no WWW-Authenticate
No auth header 401

7 new unit tests pin the predicate in both directions (too narrow → sessions leak into developer-key mode; too broad → legacy keys rejected).

547 tests passing (was 540). Lint and format clean.

Related, not fixed here

refreshToken is stored on the session (src/oauth.ts) but never read — we hold a DS refresh token and let sessions die at 24h anyway. Implementing refresh would remove this expiry cliff entirely rather than just reporting it correctly. Worth a follow-up.

When an OAuth session expired, authMiddleware fell through to the legacy
direct-developer-key branch and passed the dead session ID to the API as
if it were a key. The client got the API's "Unauthenticated." as a tool
error rather than a 401, so it never learned to re-authorise - and could
not recover even on reconnect. The only fix available to a user was to
remove and re-add the connector.

The existing 401 branch was unreachable on that path: sessionId was only
set when a session had already been found.

Recognise tokens shaped like session IDs we issue (64 lowercase hex from
randomBytes(32); developer keys are UUIDs, so no collision) and return
401 with WWW-Authenticate pointing at the resource metadata, which is the
client's cue to re-run OAuth.

Sessions have a 24h TTL, so this path is hit daily by every hosted
customer.

Verified against a locally built server in production mode:
  expired-session-shaped token -> 401 + WWW-Authenticate
  UUID developer key           -> 200, session issued, no WWW-Authenticate
  no auth header               -> 401
@ConalMullan
ConalMullan merged commit c536595 into develop Aug 12, 2026
3 checks passed
@ConalMullan
ConalMullan deleted the fix/expired-oauth-session-401 branch August 12, 2026 08:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant